<template>
  <div class="big-file-upload">
    <el-card class="box-card">
      <div slot="header"
           class="clearfix">
        <span>大文件上传</span>
      </div>
      <div class="text item">
        <el-upload class="upload-demo"
                   ref="upload"
                   action=""
                   :file-list="fileList"
                   :on-remove="removeFile"
                   :auto-upload="false"
                   :limit="1"
                   :http-request="myUpload">

          <el-button slot="trigger"
                     size="small"
                     type="primary">选取文件</el-button>
          <el-button style="margin-left: 10px;"
                     size="small"
                     type="success"
                     @click="submitUpload">上传到服务器</el-button>
          <div slot="tip"
               class="el-upload__tip">大文件上传，文件越大，上传越慢哦</div>
        </el-upload>
        <el-progress v-if="progressFlag"
                     :percentage="loadProgress"
                     :status="perStatus"></el-progress>
      </div>
    </el-card>
  </div>

</template>
 
<script>
import axios from "axios";
import SparkMD5 from "spark-md5";

export default {
  name: "BigFile",
  data() {
    return {
      fileList: [], // [{name:"",url:""}]
      fileUse: "大文件",
      fileKey: "", // 文件md5
      fileName: "", // 文件名
      shardTotal: 1, // 文件总分片数
      shardSize: 20 * 1024 * 1024, // 分片大小
      fileSzie: 1024, // 文件总大小
      currentIndex: 1, // 分片索引，当前是第几片
      suffix: "", // 文件后缀
      prefix: "", // 文件前缀
      start: 0, // 切片起始位置
      end: 0, // 切片结束位置
      checkResult: null, // 文件在服务器上的状态
      // shardListLength: 0, // 未上传分片长度
      loadProgress: 0, // 文件上传进度条
      progressFlag: false, // 是否显示进度条
      perStatus: null,
      count: 0,
    };
  },
  methods: {
    // 上传按钮事件
    async submitUpload() {
      this.count = 0;
      // 获取上传的文件对象
      const file = this.$refs.upload.uploadFiles[0];
      if (!file) {
        this.$message({
          message: `请选择上传的文件`,
        });
        return false;
      }

      this.$message({
        message: `文件计算中，请等待`,
      });

      this.progressFlag = true; // 显示进度条
      await this.getFileInfo(file);
      this.fileKey = await this.computeMD5(file);
      this.checkResult = await this.checkFile(this.fileKey, this.shardTotal);

      if (this.checkResult.fileStatus == 0) {
        // 如果服务器上没这个文件，就分片上传
        let uploadPromiseList = [];
        let count = this.myCount();
        for (let index in this.checkResult.shardList) {
          const formData = this.fileFormData(
            this.checkResult.shardList[index],
            file.raw
          );
          uploadPromiseList.push(
            this.myUpload(formData).then(() => {
              // 进度条，使用闭包稳定计数，因为不确定哪一个异步先完成
              this.loadProgress =
                (count() / this.checkResult.shardList.length).toFixed(1) * 100;
            })
          );
        }
        Promise.all(uploadPromiseList).then((res) => {
          this.uploadSuccess("文件上传成功！");
          count = null;
        });
      } else {
        // 服务器上有这个文件，直接拿返回的文件路径
        this.uploadSuccess(`极速秒传完成，文件名：${this.checkResult.name}`);
      }
    },

    // 使用闭包控制累加
    myCount() {
      let count = 0;
      return function () {
        return ++count;
      };
    },

    // 文件上传提示与进度条
    uploadSuccess(message) {
      this.$message({
        message: message,
        type: "success",
      });
      this.perStatus = "success";
      this.loadProgress = 100;
    },

    // 移除文件时，重置进度条
    removeFile() {
      this.progressFlag = false;
      this.perStatus = null;
      this.loadProgress = 0;
    },

    // 自定义上传，覆盖elementui的上传方法
    myUpload(formData) {
      return new Promise((resolve, reject) => {
        axios
          .post("/api/file/fragUploadFile", formData, {
            headers: {
              "Content-Type": "multipart/form-data",
            },
          })
          .then((res) => {
            resolve(res);
          })
          .catch((err) => {
            reject(err);
          });
      });
    },

    // 检查文件是否上传过
    checkFile(key, shardTotal) {
      return new Promise((resolve, reject) => {
        let result = {
          fileStatus: 0, // 文件状态，0:文件不存在、1:文件存在、2:文件存在但是分片没传完
          shardList: null, // 未上传的分片列表
          path: null, // 上文件传完成后的路径
          name: null,
          message: "文件消息",
        };
        let params = {
          key,
          shardTotal,
        };
        axios.post("/api/file/check", params).then((res) => {
          if (res.data.code == 200) {
            if (res.data.content.notExistShardIndexList.length > 0) {
              // 获取没有上传的分片
              result = {
                fileStatus: 0,
                shardList: res.data.content.notExistShardIndexList,
                path: null,
                name: null,
                message: "文件不完整~",
              };
            } else {
              // 获取已上传文件路径
              result = {
                fileStatus: 1,
                shardList: null,
                path: res.data.content.path,
                name: res.data.content.name,
                message: "文件已存在啦~",
              };
            }
          }
          resolve(result);
        });
      });
    },

    // 计算获取文件信息
    getFileInfo(file) {
      return new Promise((resolve, reject) => {
        // 获取文件前缀、后缀、大小
        const dotIndex = file.name.lastIndexOf(".");
        this.prefix =
          dotIndex > 0 ? file.name.substring(0, dotIndex) : file.name;
        this.suffix = dotIndex > 0 ? file.name.substring(dotIndex + 1) : "";
        this.fileSzie = file.size;
        this.fileName = file.name;
        // 总切片数
        this.shardTotal = Math.ceil(file.size / this.shardSize);
        resolve(true);
      });
    },

    // 大文件计算md5，必须分片然后存ArrayBuffer，最后计算才行
    computeMD5(file) {
      return new Promise((resolve, reject) => {
        let reader = new FileReader();
        let spark = new SparkMD5.ArrayBuffer();

        let chunkSize = 20 * 1024 * 1024;
        let chunks = Math.ceil(file.size / chunkSize);
        let currentIndex = 0;

        function loadNext() {
          let start = currentIndex * chunkSize;
          // 分片结束位置+分片大小，如果超出文件最大值，就说明是最后一片，那就取文件实际大小
          let end = Math.min(start + chunkSize, file.size);
          let bolb = file.raw.slice(start, end);
          reader.readAsArrayBuffer(bolb);
        }
        reader.onloadend = function (e) {
          spark.append(e.target.result);
          if (++currentIndex < chunks) {
            loadNext();
          } else {
            resolve(spark.end());
          }
        };
        loadNext();
      });
    },

    // 文件表单
    fileFormData(currentIndex, file) {
      let start = (currentIndex - 1) * this.shardSize;
      let end = Math.min(start + this.shardSize, file.size);
      const formData = new FormData();
      formData.append("file", file.slice(start, end));
      formData.append("fileKey", this.fileKey);
      formData.append("fileUse", 1024);
      formData.append("name", file.name);
      formData.append("shardIndex", currentIndex);
      formData.append("shardSize", this.shardSize);
      formData.append("shardTotal", this.shardTotal);
      formData.append("size", this.fileSzie);
      formData.append("suffix", this.suffix);
      return formData;
    },
  },
};
</script>
 
<style scoped>
.big-file {
  /* width: 500px; */
}
.from-box {
  margin-top: 20px;
}
.big-file-upload >>> input[type="file"]::file-selector-button {
  background-color: transparent;
  border: none;
  border-radius: 5px;
  padding: 5px 10px;
  font-weight: bold;
}
.big-file-upload >>> .el-input__inner {
  padding: 0;
  background-color: #409eff;
  border-radius: 4px;
  color: #fff;
}
.big-file-upload >>> .file-up-btn {
}
.big-file-upload >>> .file-input-btn {
}
.text {
  font-size: 14px;
}

.item {
  margin-bottom: 18px;
}

.clearfix:before,
.clearfix:after {
  display: table;
  content: "";
}
.clearfix:after {
  clear: both;
}

.box-card {
  width: 480px;
}
</style>